package com.irain.pp.tool.redis.sync;

import com.irain.pp.tool.api.syn.LockHelper;
import com.irain.pp.tool.redis.spring.PpRedisTemplate;
import com.irain.pp.tool.api.syn.SyncLockSupport;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;


/**
 * @author WangYanPeng
 * @date 2017/11/2 15:31
 */
@Component
public class RedisSyncLockImpl implements SyncLockSupport {

    @Autowired
    private PpRedisTemplate redisTemplate;

    private final RedisSerializer<String> keySerializer = new StringRedisSerializer();

    private static final String KEY_PREFIX = "LOCK:";
    private final static byte[] DEFAULT_VALUE = new byte[]{0};
    private final static long DEFAULT_INTERVAL = 10;
    private final static long RESET_EXPIRE = 1000;


    @PostConstruct
    public void init() {
        LockHelper.register(this);
    }


    @Override
    public boolean lock(Class lock, final String[] keyWords, final long expire, long timeout) {
        if (keyWords.length == 0) {
            byte[] bKey = genKey(lock, null);
            return lockForOneWithTimeout(bKey, expire, timeout, DEFAULT_INTERVAL);
        } else if (keyWords.length == 1) {
            byte[] bKey = genKey(lock, keyWords[0]);
            return lockForOneWithTimeout(bKey, expire, timeout, DEFAULT_INTERVAL);
        } else {
            Map<byte[], byte[]> tuple = genTuple(lock, keyWords, DEFAULT_VALUE);
            return lockForMultiWithTimeout(tuple, expire, timeout, DEFAULT_INTERVAL);
        }
    }

    private boolean lockForOneWithTimeout(byte[] bKey, final long expire, long timeout, long interval) {
        long end = System.currentTimeMillis() + timeout;
        if (lockForOne(bKey, expire, true)) {
            return true;
        }
        while (System.currentTimeMillis() < end) {
            try {
                Thread.sleep(interval);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            if (lockForOne(bKey, expire, false)) {
                return true;
            }
        }
        return false;
    }

    private boolean lockForOne(byte[] bKey, final long expire, boolean toCheck) {
        return redisTemplate.execute((RedisCallback<Boolean>) connection -> {
            Boolean isSuccess = connection.setNX(bKey, DEFAULT_VALUE);
            if (isSuccess) {
                connection.pExpire(bKey, expire);
                return true;
            }
            if (toCheck && connection.ttl(bKey) == -1) {
                connection.pSetEx(bKey, RESET_EXPIRE, DEFAULT_VALUE);
            }
            return false;
        });
    }


    private boolean lockForMultiWithTimeout(Map<byte[], byte[]> tuple, final long expire, long timeout, long interval) {
        long end = System.currentTimeMillis() + timeout;
        if (lockForMulti(tuple, expire, true)) {
            return true;
        }
        while (System.currentTimeMillis() < end) {
            try {
                Thread.sleep(interval);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            if (lockForMulti(tuple, expire, false)) {
                return true;
            }
        }
        return false;
    }

    private boolean lockForMulti(Map<byte[], byte[]> tuple, final long expire, boolean toCheck) {
        return redisTemplate.execute((RedisCallback<Boolean>) connection -> {
            Boolean isSuccess = connection.mSetNX(tuple);
            if (isSuccess) {
                for (byte[] bKey : tuple.keySet()) {
                    connection.pExpire(bKey, expire);
                }
                return true;
            }
            if (toCheck) {
                for (byte[] bKey : tuple.keySet()) {
                    if (connection.ttl(bKey) == -1) {
                        connection.pSetEx(bKey, RESET_EXPIRE, DEFAULT_VALUE);
                    }
                }
            }
            return false;
        });
    }

    @Override
    public void unlock(Class lock, String[] keyWords) {
        final byte[][] bKeys = genKeys(lock, keyWords);
        redisTemplate.execute((RedisCallback<Object>) connection -> {
            connection.del(bKeys);
            return null;
        });
    }

    private byte[] genKey(Class lock, String keyWord) {
        if (keyWord == null) {
            return keySerializer.serialize(KEY_PREFIX + lock.getName());
        } else {
            return keySerializer.serialize(KEY_PREFIX + lock.getName() + ":" + keyWord);
        }
    }

    private byte[][] genKeys(Class lock, String[] keyWords) {
        if (keyWords == null || keyWords.length == 0) {
            byte[][] bytes = new byte[1][];
            bytes[0] = genKey(lock, null);
            return bytes;
        }
        int len = keyWords.length;
        byte[][] bytes = new byte[len][];
        for (int i = 0; i < len; i++) {
            bytes[i] = genKey(lock, keyWords[i]);
        }
        return bytes;
    }

    private Map<byte[], byte[]> genTuple(Class lock, final String[] keyWords, final byte[] value) {
        Map<byte[], byte[]> map = new HashMap<>(16);
        if (keyWords == null || keyWords.length == 0) {
            map.put(genKey(lock, null), value);
            return map;
        }
        for (String keyWord : keyWords) {
            byte[] bKey = genKey(lock, keyWord);
            map.put(bKey, value);
        }
        return map;
    }
}
